# Configuration file parser
#
# Copyright (C) 2001-2006, 2008-2009 The Written Word, Inc.
# Copyright (C) 2000 The Written Word, LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# $Id: config.py 497 2009-04-13 18:47:38Z china $

import os
import re
import shlex
import string
import sys

from fnmatch import fnmatch
from urlparse import urlparse

from classutil import AbstractMeta, AbstractProperty, abstract
from misclib import get_gnu_systype

# exceptions
class ConfigError (Exception):
  """base class for all exceptions raised by this module"""

  def __init__ (self, msg, lineno):
    self.msg = msg
    self.lineno = lineno

  def __str__ (self):
    return self.msg

class ConfigFileNotFoundError (Exception):
  """configuration file not found"""


class ConfigBase (object):
  # Raise an exception at class instantiation time if subclasses
  # contain any (unoverriden) AbstractProperty's or @abstract methods:
  __metaclass__ = AbstractMeta

  # Set these appropriately in subclass
  _var_types = AbstractProperty ('_var_types')
  SECTION_DEPOT_VARS = AbstractProperty ('SECTION_DEPOT_VARS')
  SECTION_DIST_OPTS_VARS = AbstractProperty ('SECTION_DIST_POTS_VARS')
  SECTION_SYSTYPE_VARS = AbstractProperty ('SECTION_SYSTYPE_VARS')

  def parse (self, path):
    conf = {}

    if not os.path.exists (path):
      raise ConfigFileNotFoundError

    stream = shlex.shlex (open (path), path, posix = True)
    stream.wordchars_orig = stream.wordchars
    while 1:
      var = None
      val = None

      # get variable name or section identifier
      stream.wordchars = string.letters + string.digits + '-_'
      token = stream.get_token ()
      if not token:
        break
      if len (token) == 1 and not token in stream.wordchars:
        raise ConfigError ('invalid character in variable', stream.lineno)

      # strip trailing escape character '\'
      token = re.sub ('\\\\\n', '', token)

      if token == 'depot':
        if not conf.has_key (token):
          conf[token] = {}
        self.parse_section_depot (stream, conf)
      elif token == 'dist-opts':
        if not conf.has_key (token):
          conf[token] = {}
        self.parse_section_dist_opts (stream, conf)
      elif token == 'environment':
        if not conf.has_key (token):
          conf[token] = {}
        self.parse_section_environment (stream, conf)
      elif token == 'systype':
        self.parse_section_systype (stream, conf)
      else:
        if not self._var_types.has_key (token):
          raise ConfigError ('invalid variable name %s' %
                             token, stream.lineno)
        conf[token] = self.parse_assignment (token, stream)

    stream.instream.close ()

    return conf

  # get remainder of variable assignment
  def parse_assignment (self, var, stream):
    stream.wordchars = '='
    token = stream.get_token ()
    if not token:
      raise ConfigError ('no assignment operator ("=") for variable',
                         stream.lineno)
    if token != '=':
      raise ConfigError ('= must follow variable assignment', stream.lineno)

    # get variable value
    stream.wordchars = stream.wordchars_orig
    try:
      token = stream.get_token ()
    except ValueError, msg:
      raise ConfigError (str (msg), stream.lineno)

    if token is None:
      raise ConfigError ('no assignment for variable', stream.lineno)

    # strip trailing escape character '\'
    token = re.sub ('\\\\\n', '', token)

    if self._var_types[var] == 'array':
      # array values must begin with '['
      if token != '[':
        raise ConfigError ('"%s" accepts an array value. therefore, \
its value must be enclosed by brackets ("[]").' % var, stream.lineno)

      # read list
      val = []
      while 1:
        token = stream.get_token ()
        if token is None:
          raise ConfigError ('unterminated list assignment', stream.lineno)
        if token == ']':
          break
        val.append (token)
    elif self._var_types[var] == 'int':
      val = int (token)
    elif self._var_types[var] == 'oct':
      val = int (token, 8)
    elif self._var_types[var] == 'string':
      val = token
    elif self._var_types[var] == 'boolean':
      if token != 'false' and token != 'true':
        raise ConfigError ('%s must be a boolean "true" or "false"' % var,
                           stream.lineno)
      if token == 'false':
        val = 0
      if token == 'true':
        val = 1

    return val

  # parse depot section
  def parse_section_depot (self, stream, conf):
    while 1:
      # argument will be a URL. allow anything but whitespace in the
      # URL. The caller should be smart enough to handle an invalid name
      stream.wordchars = string.letters + string.digits + string.punctuation
      url = stream.get_token ()
      if not url:
        raise ConfigError ('end of depot section unexpected',
                           stream.lineno)

      # allow file-based URLs to begin without "file://"
      if not urlparse (url)[0]:
        url = 'file://' + url

      # make sure next token is section start symbol '{'
      stream.wordchars = '{'
      token = stream.get_token ()
      if not token:
        raise ConfigError ('end of depot section unexpected',
                           stream.lineno)
      if token != '{':
        raise ConfigError ('sections begin with {', stream.lineno)

      conf['depot'][url] = {}
      while 1:
        # get variable name or section end token
        stream.wordchars = string.letters + string.digits + '-_'
        token = stream.get_token ()
        if not token:
          raise ConfigError ('end of depot section unexpected',
                             stream.lineno)
        if token == '}':
          return
        if len (token) == 1 and not token in stream.wordchars:
          raise ConfigError ('invalid character in variable',
                             stream.lineno)

        if not self._var_types.has_key (token):
          raise ConfigError ('invalid variable name "%s"' % token,
                             stream.lineno)
        if not token in self.SECTION_DEPOT_VARS:
          raise ConfigError ('invalid variable "%s" in dist-opts section'
                             % token, stream.lineno)
        conf['depot'][url][token] = self.parse_assignment (token, stream)

  # parse dist-opts section
  def parse_section_dist_opts (self, stream, conf):
    while 1:
      # argument will be a distribution. allow anything but whitespace.
      stream.wordchars = string.letters + string.digits + string.punctuation
      dist = stream.get_token ()
      if not dist:
        raise ConfigError ('end of dist-opts section unexpected',
                           stream.lineno)

      # make sure next token is section start symbol '{'
      stream.wordchars = '{'
      token = stream.get_token ()
      if not token:
        raise ConfigError ('end of dist-opts section unexpected',
                           stream.lineno)
      if token != '{':
        raise ConfigError ('sections begin with {', stream.lineno)

      conf['dist-opts'][dist] = {}
      while 1:
        # get variable name or section end token
        stream.wordchars = string.letters + string.digits + '-_'
        token = stream.get_token ()
        if not token:
          raise ConfigError ('end of dist-opts section unexpected',
                             stream.lineno)
        if token == '}':
          return
        if len (token) == 1 and not token in stream.wordchars:
          raise ConfigError ('invalid character in variable', stream.lineno)

        if token == 'environment':
          if not conf['dist-opts'][dist].has_key (token):
            conf['dist-opts'][dist][token] = {}
          self.parse_section_environment (stream, conf, dist)
          continue
        elif token == 'systype':
          self.parse_section_systype (stream, conf, dist)
          continue

        if not self._var_types.has_key (token):
          raise ConfigError ('invalid variable name "%s"' %
                             token, stream.lineno)
        if not token in self.SECTION_DIST_OPTS_VARS:
          raise ConfigError ('invalid variable "%s" in dist-opts section' % 
                             token, stream.lineno)
        conf['dist-opts'][dist][token] = self.parse_assignment (token, stream)

  # parse environment section
  def parse_section_environment (self, stream, conf, dist = None):
    while 1:
      # make sure next token is section start symbol '{'
      stream.wordchars = '{'
      token = stream.get_token ()
      if not token:
        raise ConfigError ('end of environment section unexpected', stream.lineno)
      if token != '{':
        raise ConfigError ('sections begin with {', stream.lineno)

      while 1:
        # get environment variable name or section end token
        stream.wordchars = string.letters + string.digits + '_'
        env_var = stream.get_token ()
        if not env_var:
          raise ConfigError ('end of environment section unexpected',
                             stream.lineno)
        if env_var == '}':
          return
        if len (env_var) == 1 and not env_var in stream.wordchars:
          raise ConfigError ('invalid character in variable', stream.lineno)

        stream.wordchars = '='
        token = stream.get_token ()
        if not token:
          raise ConfigError ('no assignment operator ("=") for variable',
                             stream.lineno)
        if token != '=':
          raise ConfigError ('= must follow variable assignment',
                             stream.lineno)

        # get variable value (might contain backslash continuation
        # character)
        stream.wordchars = stream.wordchars_orig
        token = stream.get_token ()
        if token is None:
          raise ConfigError ('no assignment for variable', stream.lineno)

        # strip trailing escape character '\'
        token = re.sub ('\\\\\n', '', token)

        if dist:
          conf['dist-opts'][dist]['environment'][env_var] = token
        else:
          conf['environment'][env_var] = token

  # parse systype section
  def parse_section_systype (self, stream, conf, dist = None):
    if conf.has_key ('systype-path'):
      gnu_systype = get_gnu_systype (conf['systype-path'])
    else:
      gnu_systype = get_gnu_systype ()

    while 1:
      # argument will be a distribution. allow anything but whitespace.
      stream.wordchars = string.letters + string.digits + string.punctuation
      config_systype = stream.get_token ()
      if not config_systype:
        raise ConfigError ('end of systype section unexpected', stream.lineno)

      # make sure next token is section start symbol '{'
      stream.wordchars = '{'
      token = stream.get_token ()
      if not token:
        raise ConfigError ('end of systype section unexpected', stream.lineno)
      if token != '{':
        raise ConfigError ('sections begin with {', stream.lineno)

      while 1:
        # get variable name or section end token
        stream.wordchars = string.letters + string.digits + '-_'
        token = stream.get_token ()
        if not token:
          raise ConfigError ('end of systype section unexpected', stream.lineno)
        if token == '}':
          return
        if len (token) == 1 and not token in stream.wordchars:
          raise ConfigError ('invalid character in variable', stream.lineno)

        if not self._var_types.has_key (token):
          raise ConfigError ('invalid variable name "%s"' % token,
                             stream.lineno)
        if not token in self.SECTION_SYSTYPE_VARS:
          raise ConfigError ('invalid variable "%s" in systype section' %
                             token, stream.lineno)

        match_systype = False
        if config_systype[0] == '!':
          if not fnmatch (gnu_systype, config_systype[1:]):
            match_systype = True
        else:
          if fnmatch (gnu_systype, config_systype):
            match_systype = True

        if match_systype:
          if dist:
            conf['dist-opts'][dist][token] = self.parse_assignment (token, stream)
          else:
            conf[token] = self.parse_assignment (token, stream)
        else:
          self.parse_assignment (token, stream)
